package com.yifeng.repo.tookit.oss;


import com.google.common.base.Strings;
import com.yifeng.repo.base.utils.file.FileHelper;
import com.yifeng.repo.base.utils.file.PropertyUtils;
import com.yifeng.repo.tookit.oss.enums.FileTypeEnum;
import com.yifeng.repo.tookit.oss.model.*;
import com.yifeng.repo.tookit.oss.service.FileService;
import org.springframework.util.ObjectUtils;

import java.io.*;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class OssFileClient {
    private final String appId;
    private final String serviceUrl;
    private final String merchantId;
    private final String accessToken;
    private final String tmpFilePath;
    private volatile FileService proxy;
    private final OssFormUploader uploader = new OssFormUploader();

    public OssFileClient(String appId, String serviceUrl, String merchantId, String accessToken, String tmpFilePath) {
        this.appId = appId;
        this.serviceUrl = serviceUrl;
        this.merchantId = merchantId;
        this.accessToken = accessToken;
        this.tmpFilePath = tmpFilePath;
    }

    private FileService getFileService() {
        if (proxy == null) {
            synchronized (this) {
                if (proxy == null) {
                    proxy = new FileService(serviceUrl, merchantId, accessToken);
                }
            }
        }
        return proxy;
    }

    /**
     * 申请特别许可，支持根据type写入到特定目录下
     */
    public String uploadFileBySpecialPermit(String type, String path, File localFile) {
        String contentType = getContentType(localFile.getAbsolutePath());
        FileInputStream inputStream = getFileInputStream(localFile.getAbsolutePath());
        try {
            return this.doUploadSpecialPermit(type, path, inputStream, contentType, false);
        } finally {
            this.close(inputStream);
        }
    }

    /**
     * 转存url文件到oss
     */
    public String transferFile(String path, String url, String suffix) {
        String contentType = Mimetypes.getInstance().getMimetypeBySuffix(suffix);
        InputStream inputStream = getUrlInputStream(url);
        return this.uploadStream(appId, path, inputStream, contentType, false);
    }

    public String uploadFile(String path, File localFile) {
        return uploadFile(appId, path, localFile.getAbsolutePath(), false);
    }

    public String uploadFile(String path, String localFilePath) {
        return uploadFile(appId, path, localFilePath, false);
    }

    public String uploadFile(String appId, String path, File localFile, boolean asAttachment) {
        return this.uploadFile(appId, path, localFile.getAbsolutePath(), asAttachment);
    }

    public String uploadFile(String appId, String path, String localFilePath, boolean asAttachment) {
        String contentType = getContentType(localFilePath);
        FileInputStream inputStream = getFileInputStream(localFilePath);
        return this.uploadStream(appId, path, inputStream, contentType, asAttachment);
    }

    public String[] uploadFiles(String[] paths, String[] localFilePaths) {
        if (paths == null || localFilePaths == null || paths.length != localFilePaths.length) {
            throw new OssUploadException("Local and remote file not match! ");
        }

        List<String> result = new ArrayList<String>();
        for (int i = 0; i < paths.length; i++) {
            String remotePath = paths[i];
            String localPath = localFilePaths[i];
            try {
                String url = this.uploadFile(remotePath, localPath);
                result.add(url);
            } catch (Throwable t) {
                // clean part file
                try {
                    this.deleteFile(remotePath);
                } catch (Throwable t2) {
                    // ignore
                }

                throw new OssUploadException(localPath + " error! ", t);
            }
        }

        return result.toArray(new String[result.size()]);
    }

    public String uploadStream(String path, InputStream input) {
        return this.uploadStream(appId, path, input, "application/octet-stream", true);
    }

    public String uploadStream(String appId, String path, InputStream input) {
        return this.uploadStream(appId, path, input, "application/octet-stream", true);
    }

    public String uploadStream(String appId, String path, InputStream input, String contentType, boolean asAttachment) {
        try {
            return this.doUpload(appId, path, input, contentType, asAttachment);
        } finally {
            this.close(input);
        }
    }

    /**
     * 上传json文件数据
     *
     * @param fileName 文件名称，不带后缀名
     * @param data     数据
     * @return oss的url
     */
    public String uploadJsonData(String fileName, Object data) {
        return uploadData(fileName, data, FileTypeEnum.JSON.getKey());
    }

    /**
     * 上传属性文件数据
     *
     * @param fileName 文件名称，不带后缀名
     * @param data     属性数据
     * @return oss的url
     */
    public String uploadPropertiesData(String fileName, Map<String, Object> data) {
        return uploadData(fileName, data, FileTypeEnum.PROPERTIES.getKey());
    }

    /**
     * 传入数据和需要生成的文件类型，将最终的文件同步到oss
     *
     * @param fileName 文件名称，不带文件后缀名
     * @param data     需要同步的数据
     * @param fileType 文件类型
     * @return oss返回的url
     */
    private String uploadData(String fileName, Object data, String fileType) {
        if (Strings.isNullOrEmpty(fileName)) {
            throw new OssUploadException("fileName 不能为空");
        }
        if (Strings.isNullOrEmpty(fileType)) {
            throw new OssUploadException("fileType 不能为空");
        }
        if (Strings.isNullOrEmpty(tmpFilePath)) {
            throw new OssUploadException("临时文件目录 不能为空");
        }
        //一：生成临时文件
        String path = "/" + fileName + "." + fileType.toLowerCase();
        File tmpFile = FileHelper.createFile(tmpFilePath + path);
        //二：根据不同的数据类型将数据写入临时文件
        if (FileTypeEnum.PROPERTIES.getKey().equalsIgnoreCase(fileType)) {
            //将values转换成Map
            PropertyUtils.writeProperties(tmpFile.getAbsolutePath(), (Map<String, Object>) data);
        }
        if (FileTypeEnum.JSON.getKey().equalsIgnoreCase(fileType)) {
            PropertyUtils.writeJson(tmpFile.getAbsolutePath(), data);
        }
        //三：上传文件到OSS
        String ossUrl = uploadFile(path, tmpFile.getAbsolutePath());
        //四:删除临时属性文件
        tmpFile.delete();
        return ossUrl;
    }

    public void deleteFile(String path) {
        try {
            this.getFileService().delete(merchantId, appId, path);
        } catch (RuntimeException e) {
            throw new OssDeleteException(appId + ":" + path, e);
        }
    }

    /**
     * 获取签名
     *
     * @param signatureQuery 应用信息
     * @return 签名对象
     */
    public SignatureResult apply(SignatureQuery signatureQuery) {
        Signature apply = this.getFileService().apply(signatureQuery.getAppId(), signatureQuery.getPath());
        return new SignatureResult(signatureQuery.getAppId(), signatureQuery.getPath(), apply);
    }

    /**
     * 批量获取签名
     *
     * @param signatureQueries 应用信息
     * @return 签名对象
     */
    public List<SignatureResult> apply(List<SignatureQuery> signatureQueries) {
        if (ObjectUtils.isEmpty(signatureQueries)) {
            return Collections.emptyList();
        }
        return signatureQueries.stream().map(this::apply).collect(Collectors.toList());
    }

    private String doUpload(String appId, String path, InputStream input, String contentType, boolean asAttachment) {
        Throwable last = null;
        // enhance retry 3 times
        for (int i = 0; i < 3; i++) {
            try {
                Signature sign = getFileService().apply(appId, path);
                return uploader.uploadStream(sign, input, contentType, asAttachment);
            } catch (Throwable t) {
                if (t.getCause() != null && !Strings.isNullOrEmpty(t.getCause().getMessage()) && t.getCause().getMessage().contains("EntityTooLarge")) {
                    throw new OssUploadException("Upload " + path + " error!", t.getCause());
                }
                last = t;
                this.simpleSleep((i + 1) * 1500);
            }
        }

        throw new OssUploadException("Upload " + path + " error!", last);
    }

    private String doUploadSpecialPermit(String type, String path, InputStream input, String contentType, boolean asAttachment) {
        Throwable last = null;
        // enhance retry 3 times
        for (int i = 0; i < 3; i++) {
            try {
                Signature sign = getFileService().applySpecialPermit(type, path);
                return uploader.uploadStream(sign, input, contentType, asAttachment);
            } catch (Throwable t) {
                if (t.getCause() != null && !Strings.isNullOrEmpty(t.getCause().getMessage()) && t.getCause().getMessage().contains("EntityTooLarge")) {
                    throw new OssUploadException("Upload " + path + " error!", t.getCause());
                }
                last = t;
                this.simpleSleep((i + 1) * 1500);
            }
        }
        throw new OssUploadException("Upload " + path + " error!", last);
    }

    private FileInputStream getFileInputStream(String localFilePath) {
        try {
            return new FileInputStream(localFilePath);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    private InputStream getUrlInputStream(String url) {
        try {
            return new URL(url).openStream();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void close(Closeable cls) {
        try {
            if (cls != null) {
                cls.close();
            }
        } catch (IOException e) {
            System.err.println("OSS ERROR! " + e.getMessage());
        }
    }

    private String getContentType(String file) {
        return Mimetypes.getInstance().getMimetype(new File(file));
    }

    private void simpleSleep(int ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            // ignore
        }
    }

}
